/**
 * 
 */
package service;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import security.IAuthChecker;
import security.ServiceAuthChecker;
import service.exception.ServiceException;
import service.resources.Dao;
import bean.ParamBean;

import common.Logger;

import data.DaoFactory;
import data.IDao;
import data.exception.DaoException;

/**
 * Service Manager base class. Each service manager needs to extends this class.
 * 
 * @author chuxiaoyuan
 * 
 */
public abstract class ServiceManager implements IServiceManager {

	/**
	 * Internal Service Locator.
	 * 
	 * @author chuxiaoyuan
	 * 
	 */
	protected static class ServiceLocator {
		
		/**
		 * Constructor.
		 */
		public ServiceLocator() {
			//
		}

		/**
		 * Service stored by name.
		 */
		private Map<String, ServiceDescriptor> services = 
				new HashMap<String, ServiceDescriptor>();

		/**
		 * Get all the services.
		 * 
		 * @return services.
		 */
		public Map<String, ServiceDescriptor> getServices() {
			return this.services;
		}

		/**
		 * Add new service.
		 */
		public void addService(ServiceDescriptor serviceDescriptor) {
			this.services.put(serviceDescriptor.getService().name(),
					serviceDescriptor);
		}

		/**
		 * Add new service.
		 */
		public void addService(Service service, Method serviceHandler) {
			this.addService(new ServiceDescriptor(service, serviceHandler));
		}

		/**
		 * Look up the service by name.
		 * 
		 * @param serviceName
		 *            the service name.
		 */
		public ServiceDescriptor getService(String serviceName) {
			return this.services.get(serviceName);
		}
		
		/**
		 * Tets if there exist specified service.
		 * 
		 * @param serviceName
		 *            the service name.
		 */
		public boolean existService(String serviceName) {
			return this.services.containsKey(serviceName);
		}
	}

	/**
	 * Internal Service descriptor.
	 * 
	 * @author chuxiaoyuan
	 */
	protected static class ServiceDescriptor {

		/**
		 * Default constructor.
		 */
		public ServiceDescriptor() {
			//
		}

		/**
		 * Constructor.
		 */
		public ServiceDescriptor(Service service, Method serviceHandler) {
			this.setService(service);
			this.setServiceHandler(serviceHandler);
		}

		/**
		 * The service information.
		 */
		private Service service;

		/**
		 * The service handler.
		 */
		private Method serviceHandler;

		/**
		 * Get service information.
		 * 
		 * @return the service
		 */
		public Service getService() {
			return service;
		}

		/**
		 * Set service information.
		 * 
		 * @param service
		 *            the service to set
		 */
		public void setService(Service service) {
			this.service = service;
		}

		/**
		 * Get service handler.
		 * 
		 * @return the serviceHandler
		 */
		public Method getServiceHandler() {
			return serviceHandler;
		}

		/**
		 * Get service handler.
		 * 
		 * @param serviceHandler
		 *            the serviceHandler to set
		 */
		public void setServiceHandler(Method serviceHandler) {
			this.serviceHandler = serviceHandler;
		}
	}

	/**
	 * Service locators.
	 */
	private static Map<Class<? extends ServiceManager>, ServiceLocator> 
		SERVICE_LOCATORS = new HashMap<Class<? extends ServiceManager>, ServiceLocator>();

	/**
	 * Register service manager class.
	 * 
	 * @param clazz
	 *            the class type.
	 * @throws ExceptionInInitializerError
	 *             runtime error.
	 */
	protected static void REGISTER(Class<? extends ServiceManager> clazz) {
		
		// Register new entry.
		ServiceLocator locator = new ServiceLocator();
		SERVICE_LOCATORS.put(clazz, locator);
		
		// Get registered services
		for (Method method : clazz.getDeclaredMethods()) {

			// Get service information
			Service service = method.getAnnotation(Service.class);
			if (null != service) {
				
				// Add new service
				locator.addService(service, method);
			}
		}
	}

	/**
	 * The logger.
	 */
	private Logger logger;

	/**
	 * Get logger.
	 * 
	 * @return the logger
	 */
	protected Logger getLogger() {
		return logger;
	}

	/**
	 * Set logger.
	 * 
	 * @param logger the logger to set
	 */
	private void setLogger(Logger logger) {
		this.logger = logger;
	}

	/**
	 * Constructor.
	 * 
	 * @param request
	 *            the HTTP servlet request.
	 */
	public ServiceManager(HttpServletRequest request) {

		// Initialization
		Class<?> clazz = this.getClass();
		this.setLogger(new Logger(clazz));
		this.setDaoFactory(null);
		this.setIsDataPersistent(false);
		this.daos = new ArrayList<IDao<?>>();

		// Get service locator.
		if (!SERVICE_LOCATORS.containsKey(clazz)) {
			
			String msg = "Unregistered service manager: " + clazz;
			logger.error(msg);
			throw new ExceptionInInitializerError(msg);
		}
		this.setServiceLocator(SERVICE_LOCATORS.get(clazz));
		
		// Resources injection
		for (Field field : clazz.getDeclaredFields()) {

			// Inject DAO instance
			Dao daoField = field.getAnnotation(Dao.class);
			if (null != daoField) {

				// Initialize DAO factory.
				if (this.daoFactory == null) {
					
					this.setDaoFactory(DaoFactory.getDaoFactory());
				}

				// Create DAO instance.
				IDao<?> dao = this.daoFactory.getDao(daoField.name());
				this.getDaos().add(dao);

				// Set DAO transactional flag
				dao.setTransactional(true);

				// Inject instance.
				field.setAccessible(true);
				try {
					field.set(this, dao);

				} catch (IllegalArgumentException e) {
					
					String msg = "Failed to inject DAO: " + daoField.name();
					logger.error(msg, e);
					throw new ExceptionInInitializerError(e);

				} catch (IllegalAccessException e) {

					String msg = "Failed to inject DAO: " + daoField.name();
					logger.error(msg, e);
					throw new ExceptionInInitializerError(e);
				}

				// Set persistent flag
				this.setIsDataPersistent(true);
			}
		}
		
		// Set request.
		this.setRequest(request);
	}

	/**
	 * The service locator for this service manager.
	 */
	private ServiceLocator serviceLocator;
	
	/**
	 * The DAO factory.
	 */
	private DaoFactory daoFactory;

	/**
	 * The registered DAOs.
	 */
	private List<IDao<?>> daos;

	/**
	 * The connection.
	 */
	private Connection connection;
	
	/**
	 * Indicates if there are data persistence using DAO in this service.
	 */
	private boolean dataPersistent;

	/**
	 * The HTTP servlet request.
	 */
	private HttpServletRequest request;
	
	/**
	 * Get the current parameter bean from session.
	 * 
	 * @return null if failed to get the parameter bean.
	 */
	protected ParamBean getParamBean() {
		
		HttpSession session = this.getSession();
		if (session == null) {
			return null;
		}
		return ParamBean.getParamBean(session);
	}
	
	/**
	 * Get current session. (not create a new one)
	 * 
	 * @return the session, null if no valid session.
	 */
	protected HttpSession getSession() {
		
		// Get current session.
		return this.getRequest().getSession(false);
	}

	/**
	 * Get HTTP servlet request.
	 * 
	 * @return the request
	 */
	protected HttpServletRequest getRequest() {
		return request;
	}

	/**
	 * Set HTTP servlet request.
	 * 
	 * @param request the request to set
	 */
	private void setRequest(HttpServletRequest request) {
		this.request = request;
	}
	
	/**
	 * Get the DAO factory.
	 * 
	 * @return the daoFactory
	 */
	protected DaoFactory getDaoFactory() {
		return daoFactory;
	}

	/**
	 * Set the DAO factory.
	 * 
	 * @param daoFactory
	 *            the daoFactory to set
	 */
	private void setDaoFactory(DaoFactory daoFactory) {
		this.daoFactory = daoFactory;
	}

	/**
	 * Get the registered DAOs.
	 */
	private List<IDao<?>> getDaos() {
		return this.daos;
	}
	
	/**
	 * @return the hasDataPersistence
	 */
	protected boolean isDataPersistent() {
		return dataPersistent;
	}

	/**
	 * @param hasDataPersistence
	 *            the hasDataPersistence to set
	 */
	private void setIsDataPersistent(boolean dataPersistent) {
		this.dataPersistent = dataPersistent;
	}

	/**
	 * Get the service locator.
	 * 
	 * @return the serviceLocator
	 */
	protected ServiceLocator getServiceLocator() {
		return serviceLocator;
	}

	/**
	 * Set the service locator.
	 * 
	 * @param serviceLocator the serviceLocator to set
	 */
	private void setServiceLocator(ServiceLocator serviceLocator) {
		this.serviceLocator = serviceLocator;
	}

	/**
	 * Get the connection
	 * 
	 * @return the connection
	 */
	protected Connection getConnection() {
		return connection;
	}

	/**
	 * Roll back transaction.
	 * 
	 * @throws ServiceException roll back failed.
	 */
	protected void rollback() throws ServiceException {
		
		Connection connection = this.getConnection();
		if (connection != null) {
			try {
				// Roll back transaction.
				connection.rollback();
				
			} catch (SQLException e) {
				String msg = "Rollback failure!";
				throw new ServiceException(msg, e);
			}
		}
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see business.IService#execute(java.lang.Object)
	 */
	@Override
	public <IN, OUT> OUT execute(
			String serviceName, IN... in) throws ServiceException {

		// Verify service
		if (!this.getServiceLocator().existService(serviceName)) {
			
			String msg = "Service not exists: " + serviceName;
			logger.error(msg);
			throw new ServiceException(msg);
		}
		
		// Get the service information
		ServiceDescriptor descriptor = this.getServiceLocator().getService(serviceName);
		Service service = descriptor.getService();
		Method serviceHandler = descriptor.getServiceHandler();
		logger.log("Do service: " + serviceName + ", type=" + service.type());

		// Do authorization check before service call
		IAuthChecker auth = new ServiceAuthChecker(
				service, serviceHandler, this.getRequest());
		if (!auth.hasPermission()) {
			
			String msg = "No permission failure: " + serviceName;
			//logger.error(msg);
			throw new ServiceException(msg);
		}
		
		if (!this.isDataPersistent()) {
			
			// No data persistent required, 
			// just delegate call to the effect handler.
			return doService(serviceHandler, in);
		}
		
		// Doing different type of services.
		switch (service.type()) {
			case NoDB:
				// No Database service.
				return doService(serviceHandler, in);
				
			case Transaction:
				// Transaction service.
				return doTransaction(serviceHandler, in);
			
			case Query:
				// Query service.
				return doQuery(serviceHandler, in);
				
			default:
				String msg = "Unknown service type: " + service.type();
				//logger.error(msg);
				throw new ServiceException(msg);
		}
	}

	/**
	 * Do transaction service.
	 * 
	 * @param serviceHandler
	 *            the service handler.
	 * @param in
	 *            the service input parameter.
	 * @return the service output.
	 * @throws ServiceException
	 *             the service exception.
	 */
	private <IN, OUT> 
		OUT doTransaction(Method serviceHandler, IN... in) throws ServiceException {
		
		Connection conn = null;
		try {
			// Inject connection.
			conn = injectConnection();
			
			// Disable auto commit
			conn.setAutoCommit(false);
			
			// Do service.
			OUT out = doService(serviceHandler, in);

			// Commit.
			conn.commit();
			
			// Return service result.
			return out;
		
		} catch (SQLException e) {
			
			if (conn != null) {
				try {
					// Roll back service.
					conn.rollback();
				
				} catch (SQLException se) {

					String msg = 
							"Failed to roll back service on exception: " + e.getMessage();
					throw new ServiceException(msg, se);
				}
			}
			
			// Exception occurred.
			String msg = "Execution failed: transaction rolled back!";
			throw new ServiceException(msg, e);
			
		} catch (ServiceException e) {

			if (conn != null) {
				try {
					// Roll back service.
					conn.rollback();
				
				} catch (SQLException se) {

					String msg = 
							"Failed to roll back service on exception: " + e.getMessage();
					throw new ServiceException(msg, se);
				}
			}
			
			// Exception occurred.
			String msg = "Execution failed: transaction rolled back!";
			this.getLogger().error(msg);
			throw e;
			
		} catch (Exception e) {

			if (conn != null) {
				try {
					// Roll back service.
					conn.rollback();
				
				} catch (SQLException se) {

					String msg = 
							"Failed to roll back service on exception: " + e.getMessage();
					throw new ServiceException(msg, se);
				}
			}
			
			// Exception occurred.
			String msg = "Execution failed: transaction rolled back!";
			throw new ServiceException(msg, e);
			
		} finally {

			String msg = "";
			Exception exp = null;
			try {
				// Reset auto commit
				conn.setAutoCommit(true);
				
			} catch (SQLException e) {
				exp = e;
				msg = "Failed to reset auto commit!";
				// Continue to cloase connection.
			}
			
			// Do clean up
			this.closeConnection();
			if (exp != null) {
				throw new ServiceException(msg, exp);
			}
		}
	}

	/**
	 * Do query service.
	 * 
	 * @param serviceHandler
	 *            the service handler.
	 * @param in
	 *            the service input parameter.
	 * @return the service output.
	 * @throws ServiceException
	 *             the service exception.
	 */
	private <IN, OUT> 
		OUT doQuery(Method serviceHandler, IN... in) throws ServiceException {
		
		try {
			// Inject connection.
			injectConnection();
			
			// Do service.
			OUT out = doService(serviceHandler, in);

			// Return service result.
			return out;
		
		} finally {
			
			// Do clean up
			this.closeConnection();
		}
	}
	
	/**
	 * Create connection and do connection injection.
	 * 
	 * @return the created connection.
	 */
	private Connection injectConnection() throws ServiceException {
		
		if (null == this.connection) {

			try {
				// Open a connection
				this.connection = this.daoFactory.getConnection();
				
			} catch (DaoException e) {
				throw new ServiceException(
						"Failed to create connection!", e);
			}
		}
		
		// Injections.
		for (int i = 0; i < this.getDaos().size(); i++) {
			
			IDao<?> dao = this.getDaos().get(i);
			
			try {
				
				// Inject new connection.
				dao.setConnection(this.connection);
			
			} catch (DaoException e) {
				
				String msg = "Failed to inject connection!";
				logger.error(msg, e);
				throw new ServiceException(msg, e);
			}
		}
		
		// Return connection.
		return this.getConnection();
	}

	/**
	 * Close connection.
	 */
	private void closeConnection() throws ServiceException {
		
		if (null != this.connection) {
			try {
				// Close connection
				this.connection.close();
				
				// Reset
				this.connection = null;
				
				// Reset all DAO connections.
				for (int i = 0; i < this.getDaos().size(); i++) {
					
					IDao<?> dao = this.getDaos().get(i);
					
					try {
						dao.closeConnection();
					
					} catch (DaoException e) {
						
						String msg = "Failed to reset connections!";
						logger.error(msg, e);
						throw new ServiceException(msg, e);
					}
				}
				logger.debug("Connection closed.");
				
			} catch (SQLException e) {
				
				throw new ServiceException(
						"Failed to close connection!", e);
			}
		}
	}

	
	/**
	 * Do service.
	 * 
	 * @param in
	 *            the input parameter.
	 * @param serviceHandler
	 *            the service handler.
	 *             
	 * @return the servie output.
	 * @throws ServiceException
	 *             the service exception.
	 */
	@SuppressWarnings("unchecked")
	private <IN, OUT> 
		OUT  doService(Method serviceHandler, IN... in) throws ServiceException {
		
		try {
			
			// Delegate the service to the service handler

			// Invalid parameter
			if (in.length > MAX_ARGUMENTS) {
				String msg = 
						"Too many input parameters for service " 
								+ serviceHandler.getName();
				throw new ServiceException(msg);
			}
			
			if (in.length == 0) {

				if (Void.TYPE.equals(serviceHandler.getReturnType())) {
					
					// Return void.
					serviceHandler.invoke(this);
					return null;
					
				} else {
					// Return result
					return (OUT) serviceHandler.invoke(this);
				}
			}
			// VOID return type.
			if (Void.TYPE.equals(serviceHandler.getReturnType())) {

				if (in.length == 0) {
					// No parameter version.
					// Use the variant length arguments to enable void parameter
					serviceHandler.invoke(this);
					
				} else if (in.length == 1) {
					serviceHandler.invoke(this, in[0]);
					
				} else if (in.length == 2) {
					serviceHandler.invoke(this, in[0], in[1]);
					
				} else if (in.length == 3) {
					serviceHandler.invoke(this, in[0], in[0], in[2]);
					
				} else if (in.length == 4) {
					serviceHandler.invoke(this, in[0], in[0], in[2], in[3]);
					
				} else if (in.length == 5) {
					serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4]);
					
				} else if (in.length == 6) {
					serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5]);
					
				} else if (in.length == 7) {
					serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5], in[6]);
					
				} else if (in.length == 8) {
					serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5], in[6], in[7]);
					
				} else if (in.length == 9) {
					serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5], in[6], in[7], in[8]);
					
				} else if (in.length == 10) {
					serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5], in[6], in[7], in[8], in[9]);
				} else {
					String msg = 
							"Too many input parameters for service " 
									+ serviceHandler.getName();
					throw new ServiceException(msg);
				}
				
				// Return null for void type.
				return null;
				
			} else {
				// Return result
				if (in.length == 0) {
					// No parameter version.
					// Use the variant length arguments to enable void parameter
					return (OUT) serviceHandler.invoke(this);
					
				} else if (in.length == 1) {
					return (OUT) serviceHandler.invoke(this, in[0]);
					
				} else if (in.length == 2) {
					return (OUT) serviceHandler.invoke(this, in[0], in[1]);
					
				} else if (in.length == 3) {
					return (OUT) serviceHandler.invoke(this, in[0], in[0], in[2]);
					
				} else if (in.length == 4) {
					return (OUT) serviceHandler.invoke(this, in[0], in[0], in[2], in[3]);
					
				} else if (in.length == 5) {
					return (OUT) serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4]);
					
				} else if (in.length == 6) {
					return (OUT) serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5]);
					
				} else if (in.length == 7) {
					return (OUT) serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5], in[6]);
					
				} else if (in.length == 8) {
					return (OUT) serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5], in[6], in[7]);
					
				} else if (in.length == 9) {
					return (OUT) serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5], in[6], in[7], in[8]);
					
				} else if (in.length == 10) {
					return (OUT) serviceHandler.invoke(this, in[0], in[0], in[2], in[3], in[4], in[5], in[6], in[7], in[8], in[9]);
				} else {
					String msg = 
							"Too many input parameters for service " 
									+ serviceHandler.getName();
					throw new ServiceException(msg);
				}
				
			}
			
		} catch (IllegalAccessException e) {

			String msg = "doService failed: " + serviceHandler.getName();
			//logger.error(msg, e);
			throw new ServiceException(msg, e);
			
		} catch (IllegalArgumentException e) {

			String msg = "doService failed: " + serviceHandler.getName();
			//logger.error(msg, e);
			throw new ServiceException(msg, e);
			
		} catch (InvocationTargetException e) {

			Throwable cause = e.getCause();
			if(cause == null) {

				String msg = "doService failed: " 
						+ serviceHandler.getName() + "- InvocationTargetException";
				throw new ServiceException(msg, e);
				
	        } else if (cause instanceof ServiceException) {

	        	// User service exception
				//String msg = "doService failed: " + serviceHandler.getName() +
				//		"- User application service exception: " + cause.getMessage();
				throw (ServiceException)cause;
				
			} else if(cause instanceof Exception) {

				// Other application exception.
				String msg = "doService failed: " + serviceHandler.getName() +
						"- Other user application exception: " + cause.getMessage();
				throw new ServiceException(msg, cause);

	        } else if(cause instanceof RuntimeException) {
	        	
				String msg = "doService failed: " + serviceHandler.getName() +
						"- Runtime exception: " + cause.getMessage();
				throw new ServiceException(msg, cause);
	            
	        }  else {
	        	
	        	String msg = "doService failed: " + serviceHandler.getName() +
						"- Unknown error: " + cause.getMessage();
				throw new ServiceException(msg, e);	
	        }
			
		} catch (ClassCastException e) {

			String msg = "doService failed: " + serviceHandler.getName() +
					", incompatible service output type!";
			//logger.error(msg, e);
			throw new ServiceException(msg, e);
		
		} catch (Exception e) {
			
			// Other application exception.
			String msg = "doService failed: " 
					+ serviceHandler.getName() + " - " + e.getMessage();
			//logger.error(msg, e);
			
			// Throw ServiceException
			throw new ServiceException(msg, e);
		}
	}

}
